package com.pmdesigns.sqlmapper;

import java.io.*;
import java.util.*;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import static com.pmdesigns.sqlmapper.Utils.*;


/**
 * This class takes an annotated SQL file, a config file and
 * one or more template files as inputs and maps the tables
 * defined defined in the SQL file to model source files
 * based on the template files.
 *
 * @author mike dooley
 */
public class ModelGenerator {
	Map<String,String> typeMap = new HashMap<String,String>();
	Map<String,File> templateMap = new HashMap<String,File>();
	String sqlFile;
	File templateDir;
	File outDir;
	String defaultTemplate;
	String pkgPrefix = "";
	String defaultFileSuffix = "";
	String templateSuffix = ".tpl";
	boolean camelModelName = true;
	boolean capModelName = true;
	boolean singModelName = true;
	boolean camelFieldName = true;
	boolean capFieldName = false;
	boolean singFieldName = false;
	int ifExists = FAIL;

	private Map<String, Class> generatorClasses;
	private boolean debug;

	// ifExists values
	private static final int SKIP      = 0;
	private static final int RENAME    = 1;
	private static final int OVERWRITE = 2;
	private static final int FAIL      = 3;

	
	public static void usage() {
		System.err.println("args: <config file> [-debug]");
	}

	public static void main(String[] args) {
		if (args.length < 1 || args.length > 2) {
			System.err.println("Wrong number of args");
			usage();
			return;
		}

		boolean debug = (args.length == 2 && "-debug".equals(args[1]));
		
		try {
			ModelGenerator generator = new ModelGenerator(args[0], debug);
			generator.generate();
			
		} catch (InvocationTargetException e) {
			Throwable t = e.getTargetException();
			String msg = t.getMessage();
			System.err.println("Invocation error: "+((msg != null) ? msg : t.toString()));
			t.printStackTrace();
				
		} catch (Exception e) {
			String msg = e.getMessage();
			System.err.println("Error: "+((msg != null) ? msg : e.toString()));
			if (debug) e.printStackTrace();
		}
	}

	public ModelGenerator(String configFile, boolean debug) throws Exception {

		this.debug = debug;
		generatorClasses = new HashMap<String,Class>();

		loadConfigs(new File(configFile));

		System.out.println("Model generator configuration");
		System.out.println("                     debug:  "+debug);
		System.out.println("                  sql file:  "+sqlFile);
		System.out.println("               config file:  "+configFile);
		System.out.println("            package prefix:  "+pkgPrefix);
		System.out.println("                   out dir:  "+outDir);
		System.out.println("                 if exists:  "+((ifExists == SKIP) ? "skip" :
															(ifExists == RENAME) ? "rename" :
															(ifExists == FAIL) ? "fail" : "overwrite"));
		System.out.println("              template dir:  "+templateDir);
		System.out.println("           template suffix:  "+templateSuffix);
		System.out.println("           templates found:  "+templateMap.size());
		System.out.println("          default template:  "+defaultTemplate);
		System.out.println("   table to class name map:  "+(camelModelName?"camel ":"")+
						   (capModelName?"capitalize ":"")+(singModelName?"singularize ":""));
		System.out.println("  column to class name map:  "+(camelFieldName?"camel ":"")+
						   (capFieldName?"capitalize ":"")+(singFieldName?"singularize ":""));
		System.out.println("  column to field type map:");
		for (String key : typeMap.keySet()) {
			System.out.println("    "+key+" -> "+typeMap.get(key));
		}
	}
	
	private void loadConfigs(File propsFile) throws Exception {
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(propsFile);
			Properties props = new Properties();
			props.load(fis);
			for (Enumeration e = props.propertyNames(); e.hasMoreElements(); ) {
				String key = (String) e.nextElement();
				String val = props.getProperty(key);
				if ("sql.file".equals(key)) {
					sqlFile = val;

				} else if ("out.dir".equals(key)) {
					outDir = new File(val);

				} else if ("package.prefix".equals(key)) {
					pkgPrefix = val;
					
				} else if ("out.file.suffix".equals(key)) {
					defaultFileSuffix = val;
					
				} else if ("template.dir".equals(key)) {
					templateDir = new File(val);

				} else if ("template.suffix".equals(key)) {
					templateSuffix = val;

				} else if ("default.template".equals(key)) {
					defaultTemplate = val;
					
				} else if ("if.exists".equals(key)) {
					if      ("SKIP".equals(val))        ifExists = SKIP;
					else if ("OVERWRITE".equals(val))   ifExists = OVERWRITE;
					else if ("RENAME".equals(val))      ifExists = RENAME;
					else if ("FAIL".equals(val))        ifExists = FAIL;
					
				} else if ("table.to.model.name".equals(key)) {
					camelModelName = val.indexOf("CAMEL") != -1;
					capModelName = val.indexOf("CAPITAL") != -1;
					singModelName = val.indexOf("SINGULAR") != -1;
					
				} else if ("column.to.field.name".equals(key)) {
					camelFieldName = val.indexOf("CAMEL") != -1;
					capFieldName = val.indexOf("CAPITAL") != -1;
					singFieldName = val.indexOf("SINGULAR") != -1;

				} else {
					// assume its a colum type -> field type mapping
					typeMap.put(key, val);
				}
			}

			if (sqlFile == null) {
				throw new IllegalStateException("Missing annotated sql file");

			} else if (!new File(sqlFile).exists()) {
				throw new FileNotFoundException("Doesn't exist: "+sqlFile);
			}

			if (outDir == null) {
				throw new IllegalStateException("Missing output directory");

			} else if (!outDir.isDirectory()) {
				outDir.mkdirs();
			}
			
			if (templateDir == null) {
				throw new IllegalStateException("Missing template directory");
				
			} else if (!templateDir.isDirectory()) {
				throw new FileNotFoundException(templateDir+" is not a directory");
			}

			for (File f : templateDir.listFiles()) {
				if (f.isFile() && f.getName().endsWith(templateSuffix)) {
					templateMap.put(f.getName(), f);
				}
			}

			if (templateMap.isEmpty()) {
				throw new IllegalStateException("No templates found in: "+templateDir);
			}
			
			if (defaultTemplate == null) {
				if (templateMap.size() == 1) {
					defaultTemplate = templateMap.keySet().iterator().next();
				} else {
					throw new IllegalStateException("No default template defined.");
				}
			}

		} finally {
			if (fis != null) fis.close();
		}
	}

	public void generate() throws Exception {

		// parse the annotated sql file
		SqlParser parser = new SqlParser(sqlFile);
		Map<String,Model> models = parser.parse(); // keys are table names
		
		System.out.println("Finished parsing. Found "+models.size()+" mapped tables");

		// fill in unspecified fields and build a hash of model names to models
		Map<String,Model> modelMap = new HashMap<String,Model>();
		for (Model m : models.values()) {
			if (m.className == null) {
				m.className = table2ModelName(m.tableName);
			}
			if (modelMap.get(m.className) != null) {
				throw new IllegalStateException("Model name collision between tables: '"+
												m.tableName+"' and '"+modelMap.get(m.className).tableName+"'");
			}
			modelMap.put(m.className, m);

			if (m.classPkg == null) m.classPkg = pkgPrefix;
			if (m.template == null) m.template = defaultTemplate;
			if (m.fileSuffix == null) m.fileSuffix = defaultFileSuffix;

			// fill in field name and type
			for (Field f : m.fields) {
				if (f.fieldName == null) {
					f.fieldName = column2FieldName(f.columnName);
				}
				if (f.fieldType == null) {
					f.fieldType = mapColumnType(f);
				}
			}
		}

		System.out.println("Finished filling in default values");

		// now link up the has1, has1to1, belongs2, has1AndBelongs2 and joinedBy lists
		for (Model m : modelMap.values()) {
			for (Field f : m.fields) {
				if (f.has1 != null) {
					m.addHas1(new ForeignKey(f, findModel(f.has1, modelMap, f.beginLine)));
				}
				
				if (f.belongs2 != null) {
					findModel(f.belongs2, modelMap, f.beginLine).addHasMany(new ForeignKey(f, m));
				}
				
				if (f.has1to1 != null) {
					Model m2 = findModel(f.has1to1, modelMap, f.beginLine);
					m.addHas1(new ForeignKey(f, m2));
					m2.addBelongs2(new ForeignKey(f, m));
				}
				
				if (f.h1b2 != null) {
					Model m2 = findModel(f.h1b2, modelMap, f.beginLine);
					m.addHas1(new ForeignKey(f, m2));
					m2.addHasMany(new ForeignKey(f, m));
				}
			}

			if (m.isJoin) {
				// a join table is expected to have two has1 fields
				if (m.has1.size() != 2) {
					throw new IllegalStateException("Join table '"+m.tableName+"' on line "+m.beginLine+
													" does not have two has1 fields");
				}

				ForeignKey f1 = m.has1.get(0);
				ForeignKey f2 = m.has1.get(1);
				f1.model.addJoin(m, f1.field, f2.field, f2.model);
				f2.model.addJoin(m, f2.field, f1.field, f1.model);
			}
		}

		System.out.println("Finished resolving foreign keys");

		// now generate the code for each model
		for (Model m : modelMap.values()) {
			if (!m.isJoin) {
				generateModel(m);
			}
		}
	}

	private void generateModel(Model m) throws Exception {
		String className = appendPkg(BootstrapGenerator.GENERATOR_PKG_PREFIX,
									 template2GeneratorName(m.template));
		String methodName = BootstrapGenerator.GENERATOR_METHOD_NAME;
			
		// System.out.println("looking for generator class: "+className+", meth: "+methodName);

		// lookup the generator class
		Class genClass = generatorClasses.get(className);
		if (genClass == null) {
			genClass = Class.forName(className);
			generatorClasses.put(className, genClass);
		}

		Class[] args = {Model.class};
		Method meth = genClass.getMethod(methodName, args);
		
		// render the page
		String modelCode = (String) meth.invoke(null, m);

		// write model source code file
		File modelDir = new File(outDir, m.classPkg.replace('.', File.separatorChar));
		if (!modelDir.exists()) modelDir.mkdirs();
		File modelFile = new File(modelDir, m.className+m.fileSuffix);
		boolean overwrite = false;
		if (modelFile.exists()) {
			switch (ifExists) {
			case FAIL:
				throw new IllegalStateException("Model file exists: "+modelFile);
			case SKIP:
				System.out.println("Skipping existing model file: "+modelFile);
				return;
			case RENAME:
				File f = null;
				int i = 0;
				do {
					f = new File(modelDir, modelFile.getName()+"-"+(++i));
				} while (f.exists());

				System.out.println("Model file exists: "+modelFile+", renaming to: "+f.getName());
				if (!modelFile.renameTo(f)) {
					throw new IllegalStateException("Failed to rename existing model: "+modelFile);
				} else {
					modelFile = new File(modelDir, m.className+m.fileSuffix);
				}
				break;
			case OVERWRITE:
				overwrite = true;
				System.out.println("Overwriting model file: "+modelFile);
				break;
			}
		}

		PrintWriter pw = new PrintWriter(modelFile);
		pw.println(modelCode);
		pw.close();

		if (!overwrite) System.out.println("Wrote model source file: "+modelFile);
	}

	private String table2ModelName(String s) {
		if (camelModelName) s = camel(s);
		if (capModelName) s = capitalize(s);
		if (singModelName) s = singularize(s);
		return s;
	}
	
	private String column2FieldName(String s) {
		if (camelFieldName) s = camel(s);
		if (capFieldName) s = capitalize(s);
		if (singFieldName) s = singularize(s);
		return s;
	}

	private String mapColumnType(Field f) {
		String type = typeMap.get(f.columnType);
		if (type == null) {
			throw new IllegalStateException("Unknown mapping for column type '"+
											f.columnType+"' on line "+f.beginLine);
		}
		return type;
	}
	
	private static Model findModel(String name, Map<String,Model> map, int line) {
		Model m = map.get(name);
		if (m == null) {
			throw new IllegalStateException("Unknown foreign key model reference '"+
											name+"' on line "+line);
		}
		return m;
	}

	private static String appendPkg(String prefix, String pkg) {
		return (prefix == null || prefix.length() == 0) ? pkg : prefix+"."+pkg;
	}
}
